<?php

/**
 * MrPmvc基础数据库操作类，该类使用PDO作为引擎达到多数据库的支持
 * @author 狂奔的蜗牛
 * @email  672308444@163.com
 * @version alpha
 */
class BaseDB {

    private $dbh;
    private $sqla = array(); #sql执行记录

    /**
     * 初始化数据库连接参数
     * @param String $dsn       连接数据库的DSN字符串,与PDO类DSN完全一样
     * @param String $username  数据库用户名
     * @param String $passwd    数据库密码
     * @param Boolean $persistent TRUE使用持久连接 ，FALSE非持久连接
     */

    public function __construct($dsn, $username = '', $passwd = '', $persistent = FALSE) {
        if (strstr($dsn, 'sqlite') !== false) {
            $this->pdo_sqlite3_tester();
        }
        $options = $persistent ? array(PDO::ATTR_PERSISTENT => true) : array(PDO::ATTR_PERSISTENT => false);
        $this->dbh = new PDO($dsn, $username, $passwd, $options);
    }

    /**
     * 转义字符串
     */
    public function escape($str) {
        return $this->dbh->quote($str);
    }

    /**
     * 开始事务
     */
    public function start() {
        return $this->dbh->beginTransaction();
    }

    /**
     * 提交事务
     */
    public function commit() {
        return $this->dbh->commit();
    }

    /**
     * 事务回滚
     * @see PDO::rollBack()
     */
    public function rollBack() {
        return $this->dbh->rollBack();
    }

    /**
     * 查询一条记录.
     * @param String $pre_sql 完整的SQL查询语句，支持 ? 或 :key 替换,推荐使用替换功能，能有效防止注入
     * @param String $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     * @param String $return_type 返回的类型 hash：关联数组; array：普通数组 ; object:对象; 默认hash
     */
    public function row($pre_sql, $values = null, $return_type = 'hash') {
        return $this->result($pre_sql, $values, $return_type, true);
    }

    /**
     * 查询多条记录.
     * @param String $pre_sql 完整的SQL查询语句，支持 ? 或 :key 替换,推荐使用替换功能，能有效防止注入
     * @param String $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     * @param String $return_type 返回的类型 hash：关联数组; array：普通数组 ; object:对象; 默认hash
     */
    public function rows($pre_sql, $values = null, $return_type = 'hash') {
        return $this->result($pre_sql, $values, $return_type, false);
    }

    /**
     * 执行SQL，该方法只能用于执行一些数据库命令，不能用来执行增删改查
     * 成功返回TRUE或者一个结果数组
     * 失败返回FALSE
     */
    public function exec($sql) {
        if (preg_match('/ *(insert|update|select|delete)/i', $sql)) {
            return false;
        }
        $sta = $this->getStatementObj($sql);
        if ($this->execStatement($sta)) {
            $r = $sta->fetchAll(PDO::FETCH_NUM);
            return count($r) > 0 ? $r : true;
        }
        return false;
    }

    /**
     * 执行插入
     * 成功最后插入的id，失败返回false
     * @param String $pre_sql 从表名称开始的sql语句，不要带有insert into，函数会自动加上,支持 ? 或 :key 替换,推荐使用替换，能有效防止注入
     * @param Array $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     */
    public function insert($pre_sql, $values = null) {
        $sql = 'insert into ' . $pre_sql;
        if ($this->execStatement($this->getStatementObj($sql), $values)) {
            return $this->dbh->lastInsertId();
        } else {
            return false;
        }
    }

    /**
     * 执行更新
     * 成功返回影响的条数，失败返回-1
     * @param String $pre_sql 从表名称开始的sql语句，不要带有update，函数会自动加上,支持 ? 或 :key 替换,推荐使用替换，能有效防止注入
     * @param Array  $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     */
    public function update($pre_sql, $values = null) {
        $sql = 'update ' . $pre_sql;
        $sta = $this->getStatementObj($sql);
        if ($this->execStatement($sta, $values)) {
            return intval($sta->rowCount());
        } else {
            return false;
        }
    }

    /**
     * 执行删除
     * 成功返回影响的条数，失败返回-1
     * @param Array $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     * @param String $pre_sql 不带有delete from的SQL语句,支持 ? 或 :key 替换,推荐使用替换，能有效防止注入
     */
    public function delete($pre_sql, $values = null) {
        $sql = 'delete from ' . $pre_sql;
        $sta = $this->getStatementObj($sql);
        $count = 0;
        if (strpos($sql, 'where') === FALSE) {
            #因为大部分数据库引擎在执行不带where的delete语句的时候返回的是0，而不是表中的记录数
            #面对这种情况，这里模拟delete影响的行数,因为count(*)速度很快对性能的影响可以忽略不计
            $count = $this->count($pre_sql, $values);
        }
        if ($this->execStatement($sta, $values)) {
            if (strpos($sql, 'where') === FALSE) {
                return intval($count);
            } else {
                return intval($sta->rowCount());
            }
        } else {
            return -1;
        }
    }

    /**
     * 获取结果中第一行第一列的值
     * 通常用于获取类似select count(*) from table 这样的查询
     * @param String $sql   完整的SQL查询语句，支持 ? 或 :key 替换,推荐使用替换功能，能有效防止注入
     * @param Array $values 是一个数组,每个值对应于sql语句中的相应的?或:key的值
     */
    public function getVar($sql, $values = null) {
        $sta = $this->getStatementObj($sql);
        if ($this->execStatement($sta, $values)) {
            return $sta->fetchColumn();
        } else {
            return false;
        }
    }

    /**
     * 获取sql语句结果集的行数,
     * @param String $sql   从表名称开始的sql语句，不要带有select from，函数会自动加上
     * @param Array $values 是一个数组,每个值对应于sql语句中的相应的?或:key的值
     * 例如：$sql='user where uid<?;
     *       $values=array(90);
     */
    public function count($sql, $values = NULL) {
        $sql = 'select count(*) from ' . $sql;
        return intval($this->getVar($sql, $values));
    }

    /**
     * 获取SQL执行信息
     * @param Boolean $return TRUE返回信息数组，FALSE直接输出信息，默认FALSE
     */
    public function debug($return = false) {
        if ($return)
            return $this->sqla;
        elseif (count($this->sqla))
            echo '<h2>SQL Log:</h2>&nbsp;&nbsp;' . implode('<br/>&nbsp;&nbsp;', $this->sqla);
    }

    /**
     * 获取记录集.
     * @param String $pre_sql 完整的SQL查询语句，支持 ? 或 :key 替换,推荐使用替换功能，能有效防止注入
     * @param String $values  是一个数组,每个值对应于sql语句中的相应的?或:key的值
     * @param String $return_type 返回的类型 hash：关联数组; array：普通数组 ; object:对象; 默认hash
     * @param String $row TRUE 返回第一行; FALSE返回多行
     */
    private function result($pre_sql, $values = null, $return_type = 'hash', $row = false) {
        $sta = $this->getStatementObj($pre_sql);
        if ($this->execStatement($sta, $values)) {
            $type = array('hash' => PDO::FETCH_ASSOC
                , 'array' => PDO::FETCH_NUM
                , 'object' => PDO::FETCH_OBJ
            );
            $return_type = isset($type[strtolower($return_type)]) ? $type[strtolower($return_type)] : PDO::FETCH_ASSOC;
            if ($row) {
                return $sta->fetch($return_type);
            } else {
                return $sta->fetchAll($return_type);
            }
        } else {
            return false;
        }
    }

    /**
     * 获取sql语句对象PDOStatement，
     * 畸形的SQL语句不会通过预处理检测直接返回false，不会发生异常
     * @param String $sql 完整的SQL语句,支持 ? 或 :key 替换,推荐使用替换，能有效防止注入
     */
    private function getStatementObj($sql) {
        try {
            return $this->dbh->prepare($sql);
        } catch (Exception $e) {
            return null;
        }
    }

    /**
     * 执行"PDOStatement语句对象"
     * @param PDOStatement &$sta PDOStatement语句对象
     * @param Array $values 是一个数组,每个值对应于sql语句中的相应的?或:key的值
     */
    private function execStatement(&$sta, $values = null) {
        if (is_object($sta) && strtolower(get_class($sta)) === 'pdostatement') {
            $this->checkValues($sta->queryString, $values);
            if (is_array($values)) {
                $stra = array();
                $str = '';
                foreach ($values as $key => $val) {
                    $stra[] = "$key=>$val";
                }
                if (count($stra)) {
                    $str = ' 值 ' . implode(',', $stra);
                }
                $this->logSQL($sta->queryString . $str);
                return $sta->execute($values);
            } else {
                $this->logSQL($sta->queryString);
                return $sta->execute();
            }
        } else {
            return FALSE;
        }
    }

    /**
     * SQL执行记录
     * @param String $sql
     */
    private function logSQL($sql) {
        $this->sqla[] = $sql;
    }

    /**
     * 剔除$values中不相关的值，保证$values的里面值与$sql中的？数量对应或与:key一一对应
     * @param $sql
     * @param $values
     */
    private function checkValues($sql, &$values) {
        if (is_array($values)) {
            $j = count($values) - 1;
            foreach ($values as $key => $val) {
                if (is_numeric($key)) {
                    if (strstr($sql, '?') === FALSE) {
                        unset($values[$j--]);
                    }
                } else {
                    if (strstr($sql, $key) === FALSE) {
                        unset($values[$key]);
                    }
                }
            }
        }
    }

    /**
     * 解析出更新的SQL字符串中的表名和条件
     * @param String $sql
     * @param Array $values
     */
    private function parseUpdateStrToSelect($sql, $values) {
        $this->checkValues($sql, $values);
        if (is_array($values)) {
            foreach ($values as $key => $val) {
                if (is_numeric($key)) {
                    $sql = $this->str_seq_replace($sql, '?', $val, 0);
                } else {
                    $sql = $this->str_seq_replace($sql, $key, $val, 0);
                }
            }
        }
        $a = explode(' ', trim($sql));
        $table = $a[0];
        $where = strstr($sql, 'where');
        $sql = $table . ' ' . $where;
        return $sql;
    }

    /**
     * @name str_seq_replace(($heystack,$search,$to,$seq))
     * @param String $heystack 目标搜索字符串
     * @param String $search  关键字
     * @param String $to      替换为
     * @param Int    $seq   替换匹配关键字的次序,从0计数
     * @return string 替换后的字符串
     */
    private function str_seq_replace($heystack, $search, $to, $seq) {
        $arrStar = array();
        $sequence = 0;
        $strCompare = null;
        $arr = str_split($heystack);
        foreach ($arr as $k => $v) {
            $len = strlen($search);
            while ($len > 0 && $k < count($arr)) {
                $strCompare.=$arr[$k++];
                $len--;
            }
            if ($strCompare === $search)
                $arrStar[] = $k - strlen($search);
            $strCompare = null;
        }
        $replacedStr = substr_replace($heystack, $to, $arrStar[$seq], strlen($search));
        return $replacedStr;
    }

    private function pdo_sqlite3_tester() {
        $support_sqlite3 = false;
        try {
            $driveArr = PDO::getAvailableDrivers();
            for ($i = 0; $i < count($driveArr); $i++) {
                if ($driveArr[$i] == "sqlite") {
                    $support_sqlite3 = true;
                    break;
                }
            }
            if (!$support_sqlite3) {
                die("Please make sure Sqlite3(PDO) was enabled.");
            }
        } catch (Exception $e) {
            die("Please make sure PDO was enabled." . $e->getMessage());
        }
    }

}
